/*
 Copyright (c) 2009, Ivan Popivanov
 All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions
 are met:

 * Redistributions of source code must retain the above copyright
 notice, this list of conditions and the following disclaimer.

 * Redistributions in binary form must reproduce the above copyright
 notice, this list of conditions and the following disclaimer in the
 documentation and/or other materials provided with the distribution.

 * Neither the name of StockCandles nor the names of its contributors
 may be used to endorse or promote products derived from this software
 without specific prior written permission.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <math.h>
#include <float.h>

#include <QtGlobal>
#include <QtDebug>

#include <QtCore/QVectorIterator>

#include <QtGui/QPaintEvent>
#include <QtGui/QPainter>
#include <QtGui/QColor>

#include "Plot.h"

Plot::Plot(QWidget *parent)
    : QWidget(parent), buffer(NULL), rightMostOhlc(0), 
      plotScale(Linear), bars(0), barWidth(9), barBodyWidth(7),
      ohlcsPerBar(1),
      vAxisFont("Verdana", 12, QFont::Bold), vAxisWidth(1), majorTickWidth(5), minorTickWidth(2),
      hAxisFont("Verdana", 12, QFont::Bold), hAxisWidth(1), majorTickHeight(5), minorTickHeight(2),
      axisColor(200, 200, 200),
      vAxisTextColor(0, 0, 0),
      hAxisTextColor(0, 0, 0),
      minVLabels(6)
{
   setAttribute(Qt::WA_OpaquePaintEvent);
   initIndicators();
}

Plot::~Plot()
{
}

void
Plot::drawVAxis(QPainter &painter)
{
   painter.drawLine(0, 0, 0, ohlcArea.height());
   painter.drawLine(ohlcArea.width(), 0, ohlcArea.width(), ohlcArea.height());
}

void
Plot::drawHLines(QPainter &painter)
{
   QVectorIterator<double> iter(vAxisLabels);
   iter.toBack();
   while(iter.hasPrevious())
   {
      double y = (scaledMax - scale(iter.previous()))/(scaledMax-scaledMin)*ohlcArea.height();
      painter.drawLine(0, y, ohlcArea.width(), y);
   }
}

void
Plot::drawHAxis(QPainter &painter)
{
   painter.drawLine(0, 0, ohlcArea.width(), 0);
   painter.drawLine(0, ohlcArea.height(), ohlcArea.width(), ohlcArea.height());
}

void
Plot::drawVLines(QPainter &painter)
{
}

void
Plot::drawVAxisLabels(QPainter &painter)
{
   QVectorIterator<double> iter(vAxisLabels);

   QFontMetrics fm(vAxisFont);

   // Start from the top - should be helpful on log charts
   iter.toBack();
   while(iter.hasPrevious())
   {
      double val = iter.previous();

      // Find the y
      double y = (scaledMax - scale(val))/(scaledMax-scaledMin)*ohlcArea.height();

      double x = vAxisArea.x() + majorTickWidth + 1;

      QString str;
      // FIXME format doubles depending on the precision - needs a loop through all values first
      str.setNum(val, 'f', vLabelPrecision);

      QRect brc = fm.boundingRect(str);

      x -= brc.left();
      y -= brc.top() + brc.height() / 2;

      // Center the text around the y
      painter.setPen(vAxisTextColor);
      painter.setFont(vAxisFont);
      painter.drawText(x, y, str);
   }
}

// Adds labels from 'input' to 'added'
void
Plot::addLabels(QVector<Label> &added, QVector<Label> &input, bool allOrNone)
{
   QVectorIterator<Label> iter(input);
   iter.toBack();

   bool intersects = false;
   while(iter.hasPrevious())
   {
      const Label &l = iter.previous();

      intersects = false;
      for(int i = 0; i < added.size(); ++i)
      {
         if(added.at(i).brc.intersects(l.brc))
         {
            intersects = true;
            break;
         }
      }

      if(!intersects)
      {
         if(!allOrNone)
         {
            // painter.drawText(xx, yy, str);
            // gridLines.push_back(QLineF(x, y, x, ohlcArea.top()));
            added.push_back(l);
         }
      }
      else
      {
         // Break on the first intersection in the allOrNone case
         if(allOrNone)
         {
            break;
         }
      }
   }

   if(allOrNone && !intersects)
   {
      added += input;
   }
}

void
Plot::drawHAxisLabels(QPainter &painter)
{
   // Merge the labels that can be shown
   QVector<Label> labels;
   addLabels(labels, majorHLabels, false);
   addLabels(labels, minorHLabels, true);
   addLabels(labels, microHLabels, true);

   // Draw the labels
   QVectorIterator<Label> l(labels);
   l.toBack();
   while(l.hasPrevious())
   {
      const Label &label = l.previous();
      painter.setPen(hAxisTextColor);
      painter.setFont(hAxisFont);
      painter.drawText(label.xy, label.text);

      painter.setPen(axisColor);
      painter.drawLine(label.x, hAxisArea.top(), label.x, ohlcArea.top());
   }
}

void
Plot::paintEvent(QPaintEvent *pe)
{
   QPainter painter(this);

   painter.fillRect(0, 0, width(), height(), QColor::fromRgb(255, 255, 255));

   // No lines to draw
   if(plotLines.size() == 0) return;

   calcDrawingArea(plotLines.first());

   painter.setPen(axisColor);

   drawVAxis(painter);
   drawHAxis(painter);

   drawVLines(painter);
   drawHLines(painter);

   drawVAxisLabels(painter);
   drawHAxisLabels(painter);

   // Compute the schear factor when mapping from log coordinates to screen coordinates
   shearFactor = ohlcArea.height() / (scaledMax - scaledMin);

   QListIterator<Line> iter(plotLines);
   while(iter.hasNext())
   {
      const Line &line = iter.next();
      if(line.ohlc().size() > 0)
      {
         if(line.type() == Line::Candles)
         {
            drawCandles(painter, line, rightMostOhlc);
         }
         else if(line.type() == Line::SolidLine)
         {
            drawSolidLine(painter, line, rightMostOhlc);
         }
      }
   }
}

void
Plot::drawSolidLine(QPainter &painter, const Line &line, uint first)
{

}

void
Plot::drawCandles(QPainter &painter, const Line &line, uint first)
{
   // Compute the candles
   uint x = ohlcArea.width() - barWidth;
   uint ohlcIndex = first;

   const Ohlc &ohlc = line.ohlc();
   for(uint i = 0; i < bars && x >= ohlcArea.left() && ohlcIndex >= 0; ++i, --ohlcIndex, x -= barWidth)
   {
      double barTop, barBottom;
      if(ohlc.isRedAt(ohlcIndex))
      {
         barTop = ohlc.o[ohlcIndex];
         barBottom = ohlc.c[ohlcIndex];
      }
      else
      {
         barTop = ohlc.c[ohlcIndex];
         barBottom = ohlc.o[ohlcIndex];
      }

      QRectF openClose;
      openClose.setLeft(x);
      openClose.setTop((scaledMax - barTop)*shearFactor);
      openClose.setWidth(barBodyWidth);
      openClose.setBottom((scaledMax - barBottom)*shearFactor);

      if(ohlc.isGreenAt(ohlcIndex))
      {
         if(line.flags.testFlag(Line::FillUpCandles))
         {
            painter.fillRect(openClose, line.upColor());
         }
         else
         {
            painter.setPen(line.upColor());
            painter.drawRect(openClose);
         }

         painter.setPen(line.upColor());
         painter.drawLine(x + barBodyWidth/2, (scaledMax - ohlc.h[ohlcIndex])*shearFactor,x + barBodyWidth/2, openClose.top());
         painter.drawLine(x + barBodyWidth/2, openClose.bottom(), x + barBodyWidth/2, (scaledMax - ohlc.l[ohlcIndex])*shearFactor);
      }
      else
      {
         if(line.flags.testFlag(Line::FillDownCandles))
         {
            painter.fillRect(openClose, line.downColor());
         }
         else
         {
            painter.setPen(line.downColor());
            painter.drawRect(openClose);
         }

         painter.setPen(line.downColor());
         painter.drawLine(x + barBodyWidth/2, (scaledMax - ohlc.h[ohlcIndex])*shearFactor, x + barBodyWidth/2, openClose.top());
         painter.drawLine(x + barBodyWidth/2, openClose.bottom(), x + barBodyWidth/2, (scaledMax - ohlc.h[ohlcIndex])*shearFactor);
      }
   }
}

void
Plot::resizeEvent(QResizeEvent *re)
{
   QWidget::resizeEvent(re);
}

void
Plot::calcMinMax(const Ohlc &ohlc, const Line &mainLine)
{
   visibleMin = DBL_MAX;
   visibleMax = DBL_MIN;

   scaledMin = DBL_MAX;
   scaledMax = DBL_MIN;

   int ohlcIndex = rightMostOhlc;

   for(uint i = 0; i < bars && ohlcIndex >= 0; ++i, --ohlcIndex)
   {
      visibleMin = qMin(visibleMin, ohlc.l[ohlcIndex]);
      visibleMax = qMax(visibleMax, ohlc.h[ohlcIndex]);

      scaledMin = qMin(scaledMin, mainLine.ohlc().l[ohlcIndex]);
      scaledMax = qMax(scaledMax, mainLine.ohlc().h[ohlcIndex]);
   }
}

void
Plot::initIndicators()
{
   indicators.append(Indicator("ma,50,1,\"SolidLine,0,000000ff,,,MA (50)\""));
}

void
Plot::initLines(const Ohlc &ohlc)
{
   // FIXME This is the layout for this plot - should be loaded
   // from some configuration file - XML?
   plotLines.clear();

   Line line;
   line.setType(Line::Candles);
   line.flags = Line::FillDownCandles;
   line.setUpColor(QColor::fromRgb(0,0,0));
   plotLines.append(line);
   plotLines[0].setOhlc(ohlc);

   for(int i = 0; i < indicators.size(); ++i)
   {
      indicators[i].compute(ohlc, plotLines);
   }
}

void
Plot::calcDrawingArea(const Line &mainLine)
{
   const Ohlc &ohlc = mainLine.ohlc();
   if(ohlc.size() == 0) return;

   // The maximum number of bars visible
   bars = (width() + (barWidth - 1)) / barWidth;

   // Start with the drawing area
   ohlcArea.setRect(0, 0, width(), height());

   int vAxisAreaWidth = calcVAxisWidth();
   int hAxisAreaHeight = calcHAxisHeight();

   ohlcArea.adjust(0, 0, -vAxisAreaWidth, -hAxisAreaHeight);

   vAxisArea.setRect(ohlcArea.right(), ohlcArea.top(), vAxisAreaWidth, ohlcArea.height());
   hAxisArea.setRect(ohlcArea.left(), ohlcArea.bottom(), ohlcArea.width(), hAxisAreaHeight);

   // Compute the number of candles visible
   bars = (ohlcArea.width() + (barWidth - 1)) / barWidth;

   // Recompute the MIN and MAX
   calcMinMax(mergedOhlc, mainLine);

   calcVAxisLabels();
   calcHAxisLabels(mergedOhlc);
}

static inline int
countFractionDigits(double d, int maxDigits)
{
   int digits;
   for(digits = 0; digits < maxDigits; ++digits)
   {
      if(d == floor(d)) break;
      d *= 10;
   }
   return digits;
}

// Assumes the drawing area has been "calculated"
void
Plot::calcVAxisLabels()
{
   // Don't cover more than 80% of the axis area by labels
   QFontMetrics fm(vAxisFont);
   int maxLabels = vAxisArea.height()*0.8/fm.boundingRect("AAA").height();

   vAxisLabels.resize(0);

   int fractionId = 0;

   double exp = pow(10.0, 5.0);

   double dist = visibleMax - visibleMin;

   while(true)
   {
      // Compute the next fraction and exponent
      int newFractionId = (fractionId + 1) % DOLLAR_FRACTIONS;
      double newExp;

      if(newFractionId == 0)
      {
         newExp = exp/10.0;
      }
      else
      {
         newExp = exp;
      }

      // How many labels we will have with the new setup
      double step = dollarFractions[newFractionId]*newExp;
      double label = ceil(visibleMin/step)*step;
      int labels = 0;
      while(label > visibleMin && label < visibleMax)
      {
         ++labels;
         label += step;
      }

      if(labels > maxLabels) break;

      // The new setup is also good - remember it
      fractionId = newFractionId;
      exp = newExp;
   }

   double step = dollarFractions[fractionId]*exp;

   vLabelPrecision = 0;

   double label = ceil(visibleMin/step)*step;
   while(label < visibleMax)
   {
      vAxisLabels.push_back(label);
      label += step;

      vLabelPrecision = qMax(vLabelPrecision, countFractionDigits(label, 3));
   }
}

// Assumes the drawing area has been "calculated"
void
Plot::calcHAxisLabels(const Ohlc &ohlc)
{
   majorHLabels.resize(0);
   minorHLabels.resize(0);
   microHLabels.resize(0);

   QFontMetrics fm(vAxisFont);

   int x = ohlcArea.width();

   QDate prev;
   for(uint i = 0; i < bars; ++i, x -= barWidth)
   {
      int id = rightMostOhlc - i;
      if(i != 0)
      {
         if(ohlc.d[id].year() != prev.year())
         {
            Label l;
            l.init(fm, prev.toString("yy"), x, hAxisArea.top());
            majorHLabels.push_back(l);
         }
         else if(ohlc.d[id].month() != prev.month())
         {
            if(prev.month() == 4 || prev.month() == 7 || prev.month() == 10)
            {
               Label l;
               l.init(fm, prev.toString("MMM"), x, hAxisArea.top());
               minorHLabels.push_back(l);
            }
            else
            {
               Label l;
               l.init(fm, prev.toString("MMM"), x, hAxisArea.top());
               microHLabels.push_back(l);
            }
         }
      }
      prev = ohlc.d[id];
   }
}

void
Plot::setOhlc(const Ohlc &ohlc, int ohlcId /* = 0 */)
{
   originalOhlc.copy(ohlc);
   rightMostOhlc = ohlcId;

   // FIXME Add series compression
   // this->ohlc = originalOhlc.compress(ohlcsPerBar);
   this->mergedOhlc.copy(originalOhlc);

   Ohlc scaledOhlc;
   scaledOhlc.resize(mergedOhlc.size());
   for(int i = 0; i < mergedOhlc.size(); ++i)
   {
      scaledOhlc.setAt(i, mergedOhlc.d[i], scale(mergedOhlc.o[i]), scale(mergedOhlc.h[i]), scale(mergedOhlc.l[i]), scale(mergedOhlc.c[i]), mergedOhlc.v[i]);
   }

   initLines(scaledOhlc);

   update();
}

void
Plot::rightMostOhlcChanged(int i)
{
   this->rightMostOhlc = i;
   calcDrawingArea(plotLines.first());
   update();
}

const double Plot::dollarFractions[DOLLAR_FRACTIONS] = {1, 0.5, 0.25};